# cronmon - Cron e-mail filtering and management
# Copyright (c) 2010 Crown copyright
# 
# This file is part of cronmon.
# 
# cronmon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# cronmon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with cronmon.  If not, see <http://www.gnu.org/licenses/>.

"""Escalate a message to RT"""

import re

import cookielib
import urllib
import urllib2
import urlparse

class RT(object):
    """An interface to RT's REST API"""
    class APIError(Exception):
        """An error in making requests to the RT API"""

    class ProgrammerError(Exception):
    	"""An error in using this API"""

    class Error(Exception):
        """An error that comes from RT"""

    class AuthenticationError(Error):
        """Invalid username or password."""


    rt_code_re = re.compile(r'^RT/(?P<version>[\d.]+) (?P<code>\d{3}) (?P<message>.*)\n\n')

    def __init__(self, base_url, username=None, password=None):
    	self.base_url = base_url
        self.cookies = cookielib.CookieJar()
        self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cookies))
        self.username = username
        self.password = password
        self.authenticated = False

    def urlopen(self, urlpart, data={}, authenticate=True):
        if not self.authenticated and authenticate:
            data.update({'user': self.username, 'pass': self.password})

        if data:
            ldata = urllib.urlencode(data)
        else:
            ldata = None

        try:
            resp = self.opener.open(urlparse.urljoin(self.base_url, urlpart), ldata)
        except IOError, e:
            raise RT.APIError("Couldn't access RT REST API: %s" % e.args[0])
        else:
            return resp

    def request(self, urlpart, params={}, authenticate=True):
        content = ''
        for k, v in params.items():
            content += '%s: %s\n' % (k, str(v).replace('\n', '\n    '))

        resp = self.urlopen(urlpart, {'content': content}, authenticate=authenticate)
        content = resp.read()

        mo = self.rt_code_re.match(content)
        if not mo:
            raise RT.APIError("Malformed RT Response", content)

        code = mo.group('code')
        if authenticate and code != '401':
            self.authenticated = True

        if code == '401':
            raise RT.AuthenticationError("%(message)s" % mo.groupdict())

        if code[0] != '2':
            raise RT.Error('%(code)s %(message)s' % mo.groupdict())

        return content[mo.end():]

    def decode_kwargs(self, dict):
    	params = {}
        for k, v in dict.items():
            if v:
                rt_key = re.sub(r'(^|_)([a-z])', lambda mo: mo.group(2).upper(), k)
                params[rt_key] = v
	    return params

    def decoded_request(self, urlpart, params={}, authenticate=True):
        resp = self.request(urlpart, params, authenticate)

        lre = re.compile(r'^(\w+):\s*(.*)')

        params = {}
        k = None
        for l in resp.splitlines():
            mo = lre.match(l)
            if mo:
                k = mo.group(1)
                params[k] = mo.group(2)
            elif k:
                params[k] += '\n' + l.lstrip()
            else:
                raise RT.APIError("Could not parse RT response")

        return params

    def get_ticket(self, id):
        id = int(id)
        params = self.decoded_request('REST/1.0/ticket/%d/show' % id)
        return Ticket(self, id, params)

    def new_ticket(self, subject, queue, **kwargs):
        params = {'Subject': subject, 'Queue': queue}
        params.update(self.decode_kwargs(kwargs))
        return Ticket(self, None, params)

    def search_ids(self, query, orderby='-Priority'):
        params = {
            'query': query,
            'orderby': orderby,
            'format': 'i'
        }
        resp = self.request('REST/1.0/search/ticket?' + urllib.urlencode(params))
        regex = re.compile(r'ticket/(\d+)')
        for l in resp.splitlines():
            mo = regex.match(l)
            if mo:
                yield int(mo.group(1))

    def search(self, query, orderby='-Priority'):
        for id in self.search_ids(query, orderby):
            yield self.get_ticket(id)


class Ticket(object):
    def __init__(self, rt, id, params):
        self.rt = rt
        self.id = id
        self.params = params
        if id is None:
            self.dirty_params = set(params.keys())
        else:
            self.dirty_params = set()

    def __getitem__(self, k):
	    return self.params[k]

    def __setitem__(self, k, v):
        """Set an RT property. This tries hard to make changing a field
        a no-op if that field value hasn't changed.
        
        RT has more than one null value format, which makes this slightly
        complicated.
        """
        if k in self.params:
            if str(v) == self.params[k]:
                return
            if not v and self.params[k] == 'Not set':
                # 'Not set' is RT's idea of a null value 
                return
            self.params[k] = v
            self.dirty_params.add(k)
        else:
            if v:
                self.params[k] = v
                self.dirty_params.add(k)

    def save(self):
        if self.id is None:
            self.__create()
        else:
            self.__update()

    def __check_param(self, p):
        if p not in self.params:
            raise RT.ProgrammerError("You must set the %s parameter before creating a new ticket" % p)

    def __dirty(self):
    	return dict([(k, v) for k, v in self.params.items() if k in self.dirty_params])

    def __create(self):
        """Creates a ticket in the named queue and returns the ticket ID"""

        self.__check_param('Queue')
        self.__check_param('Subject')

        params = dict(self.params)
        params['id'] = 'ticket/new'

        resp = self.rt.request('REST/1.0/ticket/new', params)
        mo = re.match(r'# Ticket (\d+) created.', resp)
        if not mo:
            raise RT.Error('Unexpected response', resp)
        self.id = int(mo.group(1))
	
    def __update(self):
        """Creates a ticket in the named queue and returns the ticket ID"""

        params = self.__dirty()
        if not params:
            return

        params['id'] = 'ticket/%d' % self.id

        resp = self.rt.request('REST/1.0/ticket/%d/edit' % self.id, params)
        mo = re.match(r'# Ticket (\d+) updated.', resp)
        if not mo:
            raise RT.Error('Unexpected response', resp)

    def url(self):
        return urlparse.urljoin(self.rt.base_url, "Ticket/Display.html?id=%d" % self.id)

    def __repr__(self):
    	return '<Ticket: %s>' % self['Subject']

    def __unicode__(self):
        return unicode(self['Subject'])

    def __str__(self):
	    return unicode(self).encode('utf8')


if __name__ == '__main__':
    import getpass
    rt = RT('http://rt.nsonline.org.uk/', username='dpope', password=getpass.getpass())
